package edu.northwestern.cbits.purple_robot_manager.plugins; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URI; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Random; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.commons.io.FileUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.HttpHostConnectException; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.SingleClientConnManager; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.accounts.Account; import android.accounts.AccountManager; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.res.Resources; import android.net.Uri; import android.net.http.AndroidHttpClient; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v7.app.NotificationCompat; import android.util.Log; import android.widget.Toast; import edu.emory.mathcs.backport.java.util.Arrays; import edu.emory.mathcs.backport.java.util.Collections; import edu.northwestern.cbits.purple_robot_manager.EncryptionManager; import edu.northwestern.cbits.purple_robot_manager.PurpleRobotApplication; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.WiFiHelper; import edu.northwestern.cbits.purple_robot_manager.activities.StartActivity; import edu.northwestern.cbits.purple_robot_manager.activities.settings.SettingsActivity; import edu.northwestern.cbits.purple_robot_manager.logging.LiberalSSLSocketFactory; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; public class HttpUploadPlugin extends OutputPlugin { private final static String CACHE_DIR = "http_pending_uploads"; private final static String USER_HASH_KEY = "UserHash"; private final static String OPERATION_KEY = "Operation"; private final static String PAYLOAD_KEY = "Payload"; private final static String CHECKSUM_KEY = "Checksum"; private final static String CONTENT_LENGTH_KEY = "ContentLength"; private final static String STATUS_KEY = "Status"; private final static int WIFI_MULTIPLIER = 2; private final static long MAX_UPLOAD_PERIOD = 3600000; private final static long MIN_UPLOAD_PERIOD = 300000; private final static long MAX_RETRIES = 4; private final static long MAX_UPLOAD_SIZE = 262144; // 256KB private final static long MIN_UPLOAD_SIZE = 16384; // 16KB public static final String ENABLED = "config_enable_data_server"; public static final boolean ENABLED_DEFAULT = false; public static final String LAST_UPLOAD_TIME = "http_last_upload"; public static final String LAST_UPLOAD_SIZE = "http_last_upload_size"; private final List<String> _pendingSaves = new ArrayList<>(); private long _lastSave = 0; private long _lastUpload = 0; private double _throughput = 0.0; private double _accumulation = 0.0; private long _lastAccumulationMeasure = System.currentTimeMillis(); private double _accumulationSum = 0.0; private long _uploadSize = MIN_UPLOAD_SIZE; private long _uploadPeriod = MIN_UPLOAD_PERIOD; private boolean _uploading = false; private int _failCount = 0; private static SharedPreferences _preferences = null; protected static SharedPreferences getPreferences(Context context) { if (HttpUploadPlugin._preferences == null) HttpUploadPlugin._preferences = PreferenceManager.getDefaultSharedPreferences(context .getApplicationContext()); return HttpUploadPlugin._preferences; } public double getRecentThroughput() { return this._throughput; } public double getRecentAccumulation() { return this._accumulation; } private void logSuccess(boolean success) { if (success) { this._uploadSize *= 2; this._uploadPeriod /= 2; } else { this._uploadSize /= 2; this._uploadPeriod *= 2; } if (this._uploadSize > MAX_UPLOAD_SIZE) this._uploadSize = MAX_UPLOAD_SIZE; else if (this._uploadSize < MIN_UPLOAD_SIZE) this._uploadSize = MIN_UPLOAD_SIZE; if (this._uploadPeriod > MAX_UPLOAD_PERIOD) this._uploadPeriod = MAX_UPLOAD_PERIOD; else if (this._uploadPeriod < MIN_UPLOAD_PERIOD) this._uploadPeriod = MIN_UPLOAD_PERIOD; } private long savePeriod() { return 10000; } private long uploadPeriod() { long period = this._uploadPeriod; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.getContext()); long prefPeriod = Long.parseLong(prefs.getString("config_http_upload_interval", "0")); if (prefPeriod != 0) { period = prefPeriod * 1000; this._uploadPeriod = period; } return period; } private long maxUploadSize() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.getContext()); long size = Long.parseLong(prefs.getString("config_http_upload_size", "0")); if (size == 0) { int multiplier = 1; if (WiFiHelper.wifiAvailable(this.getContext())) multiplier = WIFI_MULTIPLIER; size = this._uploadSize * multiplier; } if (size < MIN_UPLOAD_SIZE) size = MIN_UPLOAD_SIZE; return size; } public String[] respondsTo() { String[] activeActions = { Probe.PROBE_READING, OutputPlugin.FORCE_UPLOAD }; return activeActions; } public void processIntent(Intent intent) { final SharedPreferences prefs = HttpUploadPlugin.getPreferences(this.getContext()); if (this.enableDataServer(prefs) == false) return; PurpleRobotApplication.fixPreferences(this.getContext(), false); if (OutputPlugin.FORCE_UPLOAD.equals(intent.getAction())) { this._lastUpload = 0; this._lastSave = 0; this._failCount = 0; final HttpUploadPlugin me = this; Runnable r = new Runnable() { public void run() { me.persistJSONObject(null); me.uploadPendingObjects(); } }; Thread t = new Thread(r); t.start(); } else { try { Bundle extras = intent.getExtras(); if (extras.containsKey("TRANSMIT") && extras.getBoolean("TRANSMIT") == false) return; final JSONObject jsonObject = OutputPlugin.jsonForBundle(extras); if (jsonObject != null) { synchronized (this._pendingSaves) { this._pendingSaves.add(jsonObject.toString()); } } else { Log.e("PR-PERSIST", "NULL JSON FOR BUNDLE " + extras); } long now = System.currentTimeMillis(); if (now - this._lastSave > this.savePeriod() || this._pendingSaves.size() > 128) { this._failCount = 0; final HttpUploadPlugin me = this; Runnable r = new Runnable() { public void run() { me.persistJSONObject(jsonObject); me.uploadPendingObjects(); } }; Thread t = new Thread(r); t.start(); } } catch (JSONException e) { LogManager.getInstance(this.getContext()).logException(e); } } } @SuppressLint("NewApi") public void uploadPendingObjects() { if (this._uploading) return; final HttpUploadPlugin me = this; final long now = System.currentTimeMillis(); if (now - me._lastUpload > me.uploadPeriod()) { this._uploading = true; final SharedPreferences prefs = HttpUploadPlugin.getPreferences(this.getContext()); PurpleRobotApplication.fixPreferences(this.getContext(), false); if (this.enableDataServer(prefs) == false) { this._uploading = false; return; } // TODO: Refactor out wifi/power restriction code to be handled solely by superclass... if (this.restrictToWifi(prefs)) { if (WiFiHelper.wifiAvailable(this.getContext()) == false) { this._throughput = 0.0; this.broadcastMessage(R.string.message_wifi_pending, false); this._lastUpload = now; this._uploading = false; return; } } final Resources resources = this.getContext().getResources(); final long maxUploadSize = me.maxUploadSize(); final Runnable r = new Runnable() { @SuppressWarnings("deprecation") public void run() { long start = System.currentTimeMillis(); boolean wasSuccessful = false; me._lastUpload = now; File pendingFolder = me.getPendingFolder(); File archiveFolder = me.getArchiveFolder(); me.broadcastMessage(R.string.message_reading_files, false); String[] filenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { return filename.endsWith(".json"); } }); if (filenames == null) filenames = new String[0]; Collections.shuffle(Arrays.asList(filenames)); ArrayList<JSONObject> pendingObjects = new ArrayList<>(); int totalRead = 0; for (String filename : filenames) { if (totalRead <= maxUploadSize) { File f = new File(pendingFolder, filename); try { byte[] bytes = EncryptionManager.getInstance().readFromEncryptedStream(me.getContext(), new FileInputStream(f), me.encryptData(prefs)); JSONArray jsonArray = new JSONArray(new String(bytes, "UTF-8")); totalRead += bytes.length; for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); pendingObjects.add(jsonObject); } } catch (JSONException | IOException e) { LogManager.getInstance(me.getContext()).logException(e); } if (me.enableArchive(prefs)) { long now = System.currentTimeMillis(); File archive = new File(archiveFolder, now + ".archive"); f.renameTo(archive); } else f.delete(); } } if (pendingObjects.size() > 0) { me.broadcastMessage(R.string.message_package_upload, false); long tally = 0; List<JSONObject> toUpload = new ArrayList<>(); for (int i = 0; i < pendingObjects.size() && tally < maxUploadSize; i++) { try { JSONObject json = pendingObjects.get(i); String jsonString = json.toString(); int jsonSize = jsonString.toString().getBytes("UTF-8").length; if (i > 0 && jsonSize > maxUploadSize) { // Skip until connection is better... } else if (i == 0 || jsonSize + tally < maxUploadSize) { tally += jsonSize; toUpload.add(json); } } catch (UnsupportedEncodingException e) { LogManager.getInstance(me.getContext()).logException(e); } } JSONArray uploadArray = new JSONArray(); for (int i = 0; i < toUpload.size(); i++) { uploadArray.put(toUpload.get(i)); } int l = 0; Random r = new Random(System.currentTimeMillis()); try { if (uploadArray.length() == 0) { while (pendingObjects.size() > 0) { JSONArray toSave = new JSONArray(); List<JSONObject> toRemove = new ArrayList<>(); for (int i = 0; i < pendingObjects.size() && i < 100; i++) { toSave.put(pendingObjects.get(i)); toRemove.add(pendingObjects.get(i)); } File f = new File(pendingFolder, "pending_" + l + ".json"); while (f.exists()) { l += r.nextInt(10); f = new File(pendingFolder, "pending_" + l + ".json"); } byte[] jsonBytes = toSave.toString().getBytes("UTF-8"); EncryptionManager.getInstance().writeToEncryptedStream(me.getContext(), new FileOutputStream(f), jsonBytes, me.encryptData(prefs)); pendingObjects.removeAll(toRemove); } throw new Exception(me.getContext().getString(R.string.error_empty_payload)); } JSONObject jsonMessage = new JSONObject(); jsonMessage.put(OPERATION_KEY, "SubmitProbes"); String payload = uploadArray.toString(); // if (Build.VERSION.SDK_INT >= // Build.VERSION_CODES.GINGERBREAD) // payload = Normalizer.normalize(payload, // Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", // ""); payload = payload.replaceAll("\r", ""); payload = payload.replaceAll("\n", ""); jsonMessage.put(PAYLOAD_KEY, payload); String userHash = EncryptionManager.getInstance().getUserHash(me.getContext()); jsonMessage.put(USER_HASH_KEY, userHash); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] checksummed = (jsonMessage.get(USER_HASH_KEY).toString() + jsonMessage.get(OPERATION_KEY).toString() + jsonMessage.get(PAYLOAD_KEY) .toString()).getBytes("UTF-8"); byte[] digest = md.digest(checksummed); String checksum = (new BigInteger(1, digest)).toString(16); while (checksum.length() < 32) { checksum = "0" + checksum; } jsonMessage.put(CHECKSUM_KEY, checksum); jsonMessage.put(CONTENT_LENGTH_KEY, checksummed.length); AndroidHttpClient androidClient = AndroidHttpClient.newInstance("Purple Robot", me.getContext()); // Liberal HTTPS setup: // http://stackoverflow.com/questions/2012497/accepting-a-certificate-for-https-on-android HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; SchemeRegistry registry = new SchemeRegistry(); registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory(); if (me.useLiberalSsl(prefs)) { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null, null); socketFactory = new LiberalSSLSocketFactory(trustStore); } registry.register(new Scheme("https", socketFactory, 443)); HttpParams params = androidClient.getParams(); HttpConnectionParams.setConnectionTimeout(params, 180000); HttpConnectionParams.setSoTimeout(params, 180000); SingleClientConnManager mgr = new SingleClientConnManager(params, registry); HttpClient httpClient = new DefaultHttpClient(mgr, params); HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); PendingIntent contentIntent = PendingIntent.getActivity(me.getContext(), 0, new Intent(me.getContext(), StartActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); String title = me.getContext().getString(R.string.notify_upload_data); NotificationCompat.Builder builder = new NotificationCompat.Builder(me.getContext()); builder.setContentTitle(title); builder.setContentText(title); builder.setContentIntent(contentIntent); builder.setSmallIcon(R.drawable.ic_note_normal); builder.setWhen(System.currentTimeMillis()); builder.setColor(0xff4e015c); Notification note = builder.build(); note.flags = Notification.FLAG_ONGOING_EVENT; String body = null; long payloadSize = -1; try { String uriString = prefs.getString("config_data_server_uri", me.getContext() .getResources().getString(R.string.sensor_upload_url)); URI siteUri = new URI(uriString); HttpPost httpPost = new HttpPost(siteUri); String jsonString = jsonMessage.toString(); List<NameValuePair> nameValuePairs = new ArrayList<>(); nameValuePairs.add(new BasicNameValuePair("json", jsonString)); HttpEntity entity = new UrlEncodedFormEntity(nameValuePairs, HTTP.UTF_8); httpPost.setEntity(entity); String uploadMessage = String.format(resources .getString(R.string.message_transmit_bytes), (httpPost.getEntity() .getContentLength() / 1024)); me.broadcastMessage(uploadMessage, false); // noteManager.notify(12345, note); HttpResponse response = httpClient.execute(httpPost); HttpEntity httpEntity = response.getEntity(); String contentHeader = null; if (response.containsHeader("Content-Encoding")) contentHeader = response.getFirstHeader("Content-Encoding").getValue(); if (contentHeader != null && contentHeader.endsWith("gzip")) { BufferedInputStream in = new BufferedInputStream( AndroidHttpClient.getUngzippedContent(httpEntity)); ByteArrayOutputStream out = new ByteArrayOutputStream(); int read = 0; byte[] buffer = new byte[1024]; while ((read = in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, read); } in.close(); body = out.toString("UTF-8"); } else body = EntityUtils.toString(httpEntity); JSONObject json = new JSONObject(body); String status = json.getString(STATUS_KEY); String responsePayload = ""; if (json.has(PAYLOAD_KEY)) responsePayload = json.getString(PAYLOAD_KEY); if (status.equals("error") == false) { byte[] responseDigest = md.digest((status + responsePayload).getBytes("UTF-8")); String responseChecksum = (new BigInteger(1, responseDigest)).toString(16); while (responseChecksum.length() < 32) { responseChecksum = "0" + responseChecksum; } if (responseChecksum.equals(json.getString(CHECKSUM_KEY))) { pendingObjects.removeAll(toUpload); wasSuccessful = true; String uploadedMessage = String.format(resources .getString(R.string.message_upload_successful), (httpPost.getEntity() .getContentLength() / 1024)); me._failCount = 0; me.broadcastMessage(uploadedMessage, false); double elapsed = ((double) (System.currentTimeMillis() - start)) / 1000.0; payloadSize = httpPost.getEntity().getContentLength(); me._throughput = ((double) payloadSize) / elapsed; } else { me.broadcastMessage(R.string.message_checksum_failed, true); me._failCount += 1; me._throughput = 0.0; } } else { String errorMessage = String.format(resources.getString(R.string.message_server_error), status); me.broadcastMessage(errorMessage, true); me._failCount += 1; me._throughput = 0.0; } } catch (HttpHostConnectException e) { me.broadcastMessage(R.string.message_http_connection_error, true); LogManager.getInstance(me.getContext()).logException(e); me._failCount += 1; me._throughput = 0.0; } catch (SocketTimeoutException e) { me.broadcastMessage(R.string.message_socket_timeout_error, true); LogManager.getInstance(me.getContext()).logException(e); me._failCount += 1; me._throughput = 0.0; } catch (SocketException e) { String errorMessage = String.format(resources.getString(R.string.message_socket_error), e.getMessage()); me.broadcastMessage(errorMessage, true); LogManager.getInstance(me.getContext()).logException(e); me._failCount += 1; me._throughput = 0.0; } catch (UnknownHostException e) { me.broadcastMessage(R.string.message_unreachable_error, true); LogManager.getInstance(me.getContext()).logException(e); me._failCount += 1; me._throughput = 0.0; } catch (JSONException e) { me.broadcastMessage(R.string.message_response_error, true); LogManager.getInstance(me.getContext()).logException(e); me._failCount += 1; me._throughput = 0.0; } catch (SSLPeerUnverifiedException e) { LogManager.getInstance(me.getContext()).logException(e); me.broadcastMessage(R.string.message_unverified_server, true); me._failCount += 1; me._throughput = 0.0; } catch (Exception e) { LogManager.getInstance(me.getContext()).logException(e); String errorMessage = String.format( resources.getString(R.string.message_general_error), e.toString()); me.broadcastMessage(errorMessage, true); me._failCount += 1; me._throughput = 0.0; } finally { androidClient.close(); } while (pendingObjects.size() > 0) { JSONArray toSave = new JSONArray(); List<JSONObject> toRemove = new ArrayList<>(); for (int i = 0; i < pendingObjects.size() && i < 100; i++) { toSave.put(pendingObjects.get(i)); toRemove.add(pendingObjects.get(i)); } File f = new File(pendingFolder, "pending_" + l + ".json"); while (f.exists()) { l += r.nextInt(10); f = new File(pendingFolder, "pending_" + l + ".json"); } byte[] jsonBytes = toSave.toString().getBytes("UTF-8"); EncryptionManager.getInstance().writeToEncryptedStream(me.getContext(), new FileOutputStream(f), jsonBytes, me.encryptData(prefs)); pendingObjects.removeAll(toRemove); } if (wasSuccessful == false && me._failCount < MAX_RETRIES) { } else { Editor e = prefs.edit(); e.putLong(HttpUploadPlugin.LAST_UPLOAD_TIME, System.currentTimeMillis()); e.putLong(HttpUploadPlugin.LAST_UPLOAD_SIZE, payloadSize); e.commit(); } String message = me.getContext().getString(R.string.notify_running); String messageTitle = me.getContext().getString(R.string.notify_running_title); builder = new NotificationCompat.Builder(me.getContext()); builder.setContentTitle(messageTitle); builder.setContentText(message); builder.setContentIntent(contentIntent); builder.setSmallIcon(R.drawable.ic_note_normal); builder.setWhen(System.currentTimeMillis()); builder.setColor(0xff4e015c); note = builder.build(); note.flags = Notification.FLAG_ONGOING_EVENT; // noteManager.notify(12345, note); } catch (IOException e) { LogManager.getInstance(me.getContext()).logException(e); } catch (KeyStoreException e) { LogManager.getInstance(me.getContext()).logException(e); } catch (CertificateException e) { LogManager.getInstance(me.getContext()).logException(e); } catch (KeyManagementException e) { LogManager.getInstance(me.getContext()).logException(e); } catch (Exception e) { LogManager.getInstance(me.getContext()).logException(e); } finally { me.logSuccess(wasSuccessful); } } me._uploading = false; filenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { return filename.endsWith(".json"); } }); if (filenames == null) filenames = new String[0]; if (me._failCount < MAX_RETRIES && filenames.length > 0) { me._lastUpload = 0; try { Thread.sleep(500); me.uploadPendingObjects(); } catch (InterruptedException e) { } } else if (me._failCount == 0) me.broadcastMessage(R.string.message_reading_complete, false); } }; Thread t = new Thread(r); t.start(); } } protected boolean useLiberalSsl(SharedPreferences prefs) { return this.coerceBoolean(prefs, "config_http_liberal_ssl", true); } protected boolean enableArchive(SharedPreferences prefs) { return this.coerceBoolean(prefs, "config_http_archive", false); } private boolean coerceBoolean(SharedPreferences prefs, String key, boolean defaultValue) { try { return prefs.getBoolean(key, defaultValue); } catch (ClassCastException e) { String enabled = prefs.getString(key, "" + defaultValue).toLowerCase(Locale.ENGLISH); boolean coerced = ("false".equals(enabled) == false); Editor edit = prefs.edit(); edit.putBoolean(key, coerced); edit.commit(); return coerced; } } private boolean restrictToWifi(SharedPreferences prefs) { return this.coerceBoolean(prefs, "config_restrict_data_wifi", true); } private boolean encryptData(SharedPreferences prefs) { return this.coerceBoolean(prefs, "config_http_encrypt", true); } private boolean enableDataServer(SharedPreferences prefs) { return this.coerceBoolean(prefs, HttpUploadPlugin.ENABLED, HttpUploadPlugin.ENABLED_DEFAULT); } protected void broadcastMessage(int stringId, boolean log) { this.broadcastMessage(this.getContext().getResources().getString(stringId), log); } public File getPendingFolder() { SharedPreferences prefs = HttpUploadPlugin.getPreferences(this.getContext()); File internalStorage = this.getContext().getFilesDir(); if (this.useExternalStorage(prefs)) internalStorage = this.getContext().getExternalFilesDir(null); if (internalStorage != null && !internalStorage.exists()) internalStorage.mkdirs(); File pendingFolder = new File(internalStorage, CACHE_DIR); if (pendingFolder != null && !pendingFolder.exists()) pendingFolder.mkdirs(); return pendingFolder; } private boolean useExternalStorage(SharedPreferences prefs) { return SettingsActivity.useExternalStorage(this.getContext()); } public File getArchiveFolder() { File f = this.getPendingFolder(); File archiveFolder = new File(f, "Archives"); if (archiveFolder != null && !archiveFolder.exists()) archiveFolder.mkdirs(); return archiveFolder; } private void persistJSONObject(final JSONObject jsonObject) { long now = System.currentTimeMillis(); this._lastSave = now; File pendingFolder = this.getPendingFolder(); String filename = now + ".json"; File f = new File(pendingFolder, filename); HashSet<String> toRemove = new HashSet<>(); HashSet<String> invalidRemove = new HashSet<>(); JSONArray saveArray = new JSONArray(); synchronized (this._pendingSaves) { for (String jsonString : this._pendingSaves) { try { JSONObject json = new JSONObject(jsonString); if (saveArray.length() < 256) { saveArray.put(json); toRemove.add(jsonString); } } catch (JSONException | OutOfMemoryError e) { LogManager.getInstance(this.getContext()).logException(e); invalidRemove.add(jsonString); } } this._pendingSaves.removeAll(invalidRemove); } try { byte[] jsonBytes = saveArray.toString().getBytes("UTF-8"); SharedPreferences prefs = HttpUploadPlugin.getPreferences(this.getContext()); EncryptionManager.getInstance().writeToEncryptedStream(this.getContext(), new FileOutputStream(f), jsonBytes, this.encryptData(prefs)); this._accumulationSum += jsonBytes.length; if (now - this._lastAccumulationMeasure > 10000) { long duration = (now - this._lastAccumulationMeasure) / 1000; this._accumulation = this._accumulationSum / duration; this._accumulationSum = 0; this._lastAccumulationMeasure = now; } synchronized (this._pendingSaves) { this._pendingSaves.removeAll(toRemove); } } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } catch (OutOfMemoryError | IOException e) { LogManager.getInstance(this.getContext()).logException(e); } if (this._pendingSaves.size() > 128) { this._lastSave = 0; this._failCount = 0; final HttpUploadPlugin me = this; Runnable r = new Runnable() { public void run() { me.persistJSONObject(null); } }; Thread t = new Thread(r); t.start(); } } public void mailArchiveFiles(final Context context) { if (context instanceof Activity) { Activity activity = (Activity) context; activity.runOnUiThread(new Runnable() { public void run() { Toast.makeText(context, "Packaging archive files for mailing...", Toast.LENGTH_LONG).show(); } }); } final HttpUploadPlugin me = this; Runnable r = new Runnable() { public void run() { File storage = context.getExternalCacheDir(); if (!storage.exists()) storage.mkdirs(); File pendingFolder = me.getArchiveFolder(); final File[] pendingFiles = pendingFolder.listFiles(new FileFilter() { public boolean accept(File file) { return file.getName().toLowerCase(Locale.getDefault()).endsWith(".archive"); } }); final File zipfile = new File(storage, "archives.zip"); final ArrayList<File> toDelete = new ArrayList<>(); try { ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(zipfile)); int totalWritten = 0; for (int i = 0; i < pendingFiles.length && totalWritten < (MAX_UPLOAD_SIZE * 5 * 4); i++) { File f = pendingFiles[i]; String filename = f.getName(); FileInputStream fin = new FileInputStream(f); ZipEntry entry = new ZipEntry(filename); zout.putNextEntry(entry); byte[] buffer = new byte[2048]; int read = 0; while ((read = fin.read(buffer, 0, buffer.length)) != -1) { zout.write(buffer, 0, read); totalWritten += read; } fin.close(); zout.closeEntry(); toDelete.add(f); } zout.close(); } catch (IOException e) { LogManager.getInstance(me.getContext()).logException(e); } if (context instanceof Activity) { Activity activity = (Activity) context; activity.runOnUiThread(new Runnable() { public void run() { AccountManager accountManager = AccountManager.get(context); String email = null; Account[] accounts = accountManager.getAccountsByType("com.google"); for (int i = 0; i < accounts.length && email == null; i++) { Account account = accounts[i]; email = account.name; } Uri fileUri = Uri.fromFile(zipfile); Intent sendIntent = new Intent(Intent.ACTION_SEND); sendIntent.setType("application/zip"); sendIntent.putExtra(Intent.EXTRA_SUBJECT, "Purple Robot Archives"); sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri); if (email != null) { String[] emails = {email}; sendIntent.putExtra(Intent.EXTRA_EMAIL, emails); } context.startActivity(sendIntent); int remaining = pendingFiles.length - toDelete.size(); Toast.makeText(context, toDelete.size() + " archives packaged, " + remaining + " left in the device.", Toast.LENGTH_LONG).show(); for (File f : toDelete) { f.delete(); } } }); } } }; Thread t = new Thread(r); t.start(); } @SuppressLint("DefaultLocale") public void deleteArchiveFiles(final Context context) { final HttpUploadPlugin me = this; Runnable r = new Runnable() { public void run() { File pendingFolder = me.getArchiveFolder(); if (context instanceof Activity) { Activity activity = (Activity) context; activity.runOnUiThread(new Runnable() { public void run() { Toast.makeText(context, context.getString(R.string.message_clearing_archive), Toast.LENGTH_LONG).show(); } }); } try { FileUtils.deleteDirectory(pendingFolder); } catch (IOException e) { LogManager.getInstance(context).logException(e); } } }; Thread t = new Thread(r); t.start(); } public int pendingFilesCount() { File pendingFolder = this.getPendingFolder(); String[] filenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { return filename.endsWith(".json"); } }); if (filenames == null) filenames = new String[0]; return filenames.length; } public static void clearFiles(Context context) { try { File internalStorage = context.getFilesDir(); File internalFolder = new File(internalStorage, CACHE_DIR); if (internalFolder.exists()) FileUtils.deleteDirectory(internalFolder); File externalStorage = context.getExternalFilesDir(null); File externalFolder = new File(externalStorage, CACHE_DIR); if (externalFolder.exists()) FileUtils.deleteDirectory(externalFolder); } catch (IOException e) { LogManager.getInstance(context).logException(e); } } public long pendingFilesSize() { File pendingFolder = this.getPendingFolder(); String[] filenames = pendingFolder.list(new FilenameFilter() { public boolean accept(File dir, String filename) { return filename.endsWith(".json"); } }); if (filenames.length < 1024) { try { return FileUtils.sizeOf(pendingFolder); } catch (IllegalArgumentException e) { } } return 2L * 1024 * 1024 * 1024; } public boolean isEnabled(Context context) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); return prefs.getBoolean(HttpUploadPlugin.ENABLED, HttpUploadPlugin.ENABLED_DEFAULT); } }